'************************************************************************************
' Copyright © 2003-2005 Kathleen Dollard
'
' This software is provided 'as-is', without any express or implied warranty. In no 
' event will the author be held liable for any damages arising from the use of this
' software.
' 
' You may use this software for any purpose, including commercial applications, 
' and you may redistribute this software as part of application delivery: however, 
' this copyright notice may not be removed and you may not otherwise redistribute 
' this software directly other than providing a link to wwww.gendotnet.com. 
'
' You may alter this software to better server your needs (I'd like to hear about your 
' changes in order to make the software better). If you alter the softwre, you must  
' clearly mark it as altered and must not be misrepresented as the original software.
' nor wholly your original work.
'*************************************************************************************
Option Strict On
Option Explicit On 

Imports System

Namespace KADGen.Utility
   Public Class CompareXMLFiles

      Private Enum ChangeType
         RightAttributeMissing
         LeftAttributeMissing
         RightElementMissing
         LeftElementMissing
         RightAmbiguousElement
         LeftAmbiguousElement
         AttributeValuesDifferent
         ChangedNode
      End Enum

#Region "Class level declarations"
      Private mKnownChangeXMLDoc As Xml.XmlDocument
      Private mKnownChanges As KnownChangeCollection
      Private mNsMgr As Xml.XmlNamespaceManager
      Private mLogDoc As Xml.XmlDocument
#End Region

      '#Region "Constructors"
      '#End Region

#Region "Public properties and methods"
      Public Function CompareXMLDocs( _
                ByVal nodeLog As Xml.XmlNode, _
                ByVal nodeLeft As Xml.XmlDocument, _
                ByVal nodeRight As Xml.XmlDocument, _
                ByVal nodeKnownChanges As Xml.XmlNode) _
                As Boolean
         SetNamespaceManager(nodeLeft, nodeRight)
         mLogDoc = nodeLog.OwnerDocument
         If mLogDoc Is Nothing Then
            mLogDoc = CType(nodeLog, Xml.XmlDocument)
         End If
         Me.mKnownChanges = New KnownChangeCollection(nodeKnownChanges, nodeLeft, nodeRight)
         CompareXMLNodes(nodeLog, nodeLeft, nodeRight)
      End Function
#End Region

#Region "Private properties and methods"
      Private Function CompareXMLNodes( _
                ByVal nodeLog As Xml.XmlNode, _
                ByVal nodeLeft As Xml.XmlNode, _
                ByVal nodeRight As Xml.XmlNode) _
                As Boolean
         ' NOTE! The log is not appended unless the return value is also True
         Dim attrRight As Xml.XmlAttribute
         Dim childRight As Xml.XmlNode
         Dim ret As Boolean
         Dim newRecord As Record
         Dim children As Xml.XmlNodeList
         Dim xPath As String
         If Not nodeLeft.Attributes Is Nothing Then
            ret = RecordMissingAttributes(nodeLog, nodeLeft, nodeRight, "Right") Or ret
            ret = RecordMissingAttributes(nodeLog, nodeRight, nodeLeft, "Left") Or ret
            For Each attrLeft As Xml.XmlAttribute In nodeLeft.Attributes
               childRight = nodeRight.Attributes.GetNamedItem(attrLeft.Name)
               If Not childRight Is Nothing Then 'if it is nothing, we already logged
                  attrRight = CType(childRight, Xml.XmlAttribute)
                  If attrRight.Value <> attrLeft.Value Then
                     newRecord = New Record(mLogDoc, ChangeType.AttributeValuesDifferent, nodeLeft.Name, attrLeft.Name)
                     newRecord.AddAttribute("LeftValue", attrLeft.Value)
                     newRecord.AddAttribute("RightValue", attrRight.Value)
                     ret = RecordChange(newRecord, nodeLog, nodeLeft, nodeRight) Or ret
                  End If
               End If
            Next
         End If

         ret = RecordMissingElements(nodeLog, nodeLeft, nodeRight, "Right") Or ret
         ret = RecordMissingElements(nodeLog, nodeRight, nodeLeft, "Left") Or ret
         For Each childLeft As Xml.XmlNode In nodeLeft.ChildNodes
            xPath = GetXPath(childLeft)
            children = nodeRight.SelectNodes(xPath, mNsMgr)
            If children.Count = 1 Then ' otherwise, already logged
               newRecord = New Record(mLogDoc, ChangeType.ChangedNode, childLeft.Name, childLeft.Name)
               newRecord.AddAttribute("XPath", xPath)
               If CompareXMLNodes(newRecord.Node, childLeft, children(0)) Then
                  ret = RecordChange(newRecord, nodeLog, nodeLeft, nodeRight) Or ret
               End If
            End If
         Next
         Return ret
      End Function

      Private Function RecordMissingAttributes( _
               ByVal nodeLog As Xml.XmlNode, _
               ByVal nodeFirst As Xml.XmlNode, _
               ByVal nodeSecond As Xml.XmlNode, _
               ByVal direction As String) _
               As Boolean
         Dim attrNode As Xml.XmlNode
         Dim ret As Boolean
         Dim newRecord As Record
         Dim changeType As changeType
         For Each attrFirst As Xml.XmlAttribute In nodeFirst.Attributes
            attrNode = nodeSecond.Attributes.GetNamedItem(attrFirst.Name)
            If attrNode Is Nothing Then
               If direction.ToLower = "left" Then
                  changeType = changeType.LeftAttributeMissing
               Else
                  changeType = changeType.RightAttributeMissing
               End If
               newRecord = New Record(mLogDoc, changeType, nodeFirst.Name, attrFirst.Name)
               ret = RecordChange(newRecord, nodeLog, nodeFirst, nodeSecond) Or ret
            End If
         Next
         Return ret
      End Function

      Private Function RecordMissingElements( _
               ByVal nodeLog As Xml.XmlNode, _
               ByVal nodeFirst As Xml.XmlNode, _
               ByVal nodeSecond As Xml.XmlNode, _
               ByVal direction As String) _
               As Boolean
         Dim children As Xml.XmlNodeList
         Dim xpath As String
         Dim ret As Boolean
         Dim newRecord As Record
         Dim changeType As changeType
         If Not nodeFirst.ChildNodes Is Nothing Then
            For Each childFirst As Xml.XmlNode In nodeFirst.ChildNodes
               xpath = GetXPath(childFirst)
               children = nodeSecond.SelectNodes(xpath, mNsMgr)
               If children.Count > 1 Then
                  If direction.ToLower = "left" Then
                     changeType = changeType.LeftAmbiguousElement
                  Else
                     changeType = changeType.RightAmbiguousElement
                  End If
                  newRecord = New Record(mLogDoc, changeType, childFirst.Name, childFirst.Name)
                  newRecord.AddAttribute("XPath", xpath)
                  newRecord.AddAttribute("Count", nodeFirst.SelectNodes(xpath, mNsMgr).Count.ToString)
                  newRecord.AddAttribute(direction & "Count", children.Count.ToString)
                  ret = RecordChange(newRecord, nodeLog, nodeFirst, nodeSecond) Or ret
               ElseIf children.Count = 0 Then
                  If direction.ToLower = "left" Then
                     changeType = changeType.LeftElementMissing
                  Else
                     changeType = changeType.RightElementMissing
                  End If
                  newRecord = New Record(mLogDoc, changeType, childFirst.Name, childFirst.Name)
                  newRecord.AddAttribute("XPath", xpath)
                  ret = RecordChange(newRecord, nodeLog, nodeFirst, nodeSecond) Or ret
               End If
            Next
         End If
         Return ret
      End Function

      Private Function GetXPath(ByVal node As Xml.XmlNode) As String
         Dim xpath As String
         If node.Attributes Is Nothing Then
            xpath = node.Name
         Else
            xpath = GetXPath(node, "UniqueName")
            If xpath Is Nothing Then
               xpath = GetXPath(node, "Name", "Table")
               If xpath Is Nothing Then
                  xpath = GetXPath(node, "Grantee")
                  If xpath Is Nothing Then
                     xpath = GetXPath(node, "RightTable")
                     If xpath Is Nothing Then
                        xpath = GetXPath(node, "RightColumn")
                        If xpath Is Nothing Then
                           xpath = node.Name
                        End If
                     End If
                  End If
               End If
            End If
         End If
         Return xpath
      End Function

      Private Function GetXPath( _
               ByVal node As Xml.XmlNode, _
               ByVal ParamArray texts() As String) _
               As String
         Dim nodeAttr As Xml.XmlNode
         Dim attrName As Xml.XmlAttribute
         Dim ret As String = ""
         Dim cnt As Int32
         For Each text As String In texts
            nodeAttr = node.Attributes.GetNamedItem(text)
            If Not nodeAttr Is Nothing Then
               attrName = CType(nodeAttr, Xml.XmlAttribute)
               If cnt > 0 Then
                  ret &= " and "
               End If
               ret &= "@" & text & "='" & attrName.Value & "'"
               cnt += 1
            End If
         Next
         If ret.Length > 0 Then
            Return node.Name & "[" & ret & "]"
         End If
         Return Nothing
      End Function

      Private Sub SetNamespaceManager(ByVal nodeLeft As Xml.XmlNode, ByVal nodeRight As Xml.XmlNode)
         Dim leftDoc As Xml.XmlDocument = nodeLeft.OwnerDocument
         Dim rightDoc As Xml.XmlDocument = nodeLeft.OwnerDocument
         If rightDoc Is Nothing Then
            rightDoc = CType(nodeRight, Xml.XmlDocument)
         End If
         If leftDoc Is Nothing Then
            leftDoc = CType(nodeLeft, Xml.XmlDocument)
         End If
         mNsMgr = Utility.xmlHelpers.BuildNamespacesManagerForDoc(leftDoc)
         Utility.xmlHelpers.BuildNamespacesManagerForDoc(rightDoc, mNsMgr)
      End Sub

      Private Function RecordChange( _
               ByVal newRecord As Record, _
               ByVal nodeLog As Xml.XmlNode, _
               ByVal nodeFirst As Xml.XmlNode, _
               ByVal nodeSecond As Xml.XmlNode) _
               As Boolean
         Dim skip As Boolean
         Dim node As Xml.XmlNode = Nothing
         For Each knownChange As knownChange In Me.mKnownChanges
            If knownChange.ChangeType = newRecord.ChangeType Then
               If knownChange.ElementNames.Contains(newRecord.ElementName) Then
                  If knownChange.ItemName = newRecord.Name Then
                     Try
                        If (knownChange.OutputXPathTest Is Nothing OrElse _
                                 knownChange.OutputXPathTest.Trim.Length = 0) And _
                           (knownChange.NodeXPathTestLeft Is Nothing OrElse _
                                 knownChange.NodeXPathTestLeft.Trim.Length = 0) And _
                           (knownChange.NodeXPathTestRight Is Nothing OrElse _
                                 knownChange.NodeXPathTestRight.Trim.Length = 0) Then
                           skip = True
                           Exit For
                        ElseIf Not knownChange.OutputXPathTest Is Nothing AndAlso _
                                 knownChange.OutputXPathTest.Trim.Length > 0 Then
                           node = newRecord.Node
                           If Not node.SelectSingleNode(knownChange.OutputXPathTest) Is Nothing Then
                              skip = True
                              Exit For
                           End If
                        ElseIf Not knownChange.NodeXPathTestLeft Is Nothing AndAlso _
                                 knownChange.NodeXPathTestLeft.Trim.Length > 0 Then
                           node = newRecord.Node
                           If Not node.SelectSingleNode(knownChange.NodeXPathTestLeft) Is Nothing Then
                              skip = True
                              Exit For
                           End If
                        ElseIf Not knownChange.NodeXPathTestRight Is Nothing AndAlso _
                                 knownChange.NodeXPathTestRight.Trim.Length > 0 Then
                           node = newRecord.Node
                           If Not node.SelectSingleNode(knownChange.NodeXPathTestRight) Is Nothing Then
                              skip = True
                              Exit For
                           End If
                        End If
                     Catch ex As Xml.Xsl.XsltException
                        'Diagnostics.Debug.WriteLine("")
                        ' KADLOG
                        Throw
                        ' KAD: Log problem and continue
                     End Try
                     'If knownChange.OutputXPathTest Is Nothing OrElse _
                     '         knownChange.OutputXPathTest.Trim.Length = 0 Then
                     '   skip = True
                     '   Exit For
                     'Else
                     '   node = newRecord.Node
                     '   If Not node.SelectSingleNode(knownChange.OutputXPathTest) Is Nothing Then
                     '      skip = True
                     '      Exit For
                     '   End If
                     'End If
                  End If
               End If
            End If
         Next
         If Not skip Then
            If node Is Nothing Then
               node = newRecord.Node
            End If
            nodeLog.AppendChild(node)
            Return True
         End If
      End Function

      Private Class KnownChangeCollection
         Inherits Collections.CollectionBase

         Public Sub New( _
                  ByVal nodeMetadataChanges As Xml.XmlNode, _
                  ByVal nodeOld As Xml.XmlNode, _
                  ByVal nodeNew As Xml.XmlNode)
            Dim nodeList As Xml.XmlNodeList
            Dim xPath As String
            Dim verOld As Double
            Dim verNew As Double
            Dim nsMgr As Xml.XmlNamespaceManager
            Dim xmlDoc As Xml.XmlDocument
            If nodeMetadataChanges.OuterXml.Trim.Length > 0 Then
               xmlDoc = nodeMetadataChanges.OwnerDocument
               If xmlDoc Is Nothing Then
                  xmlDoc = CType(nodeMetadataChanges, Xml.XmlDocument)
               End If
               nsMgr = xmlHelpers.BuildNamespacesManagerForDoc(xmlDoc)
               verOld = Tools.GetAttributeFloatOrZero(nodeOld, "VersionID")
               verNew = Tools.GetAttributeFloatOrZero(nodeNew, "VersionID")
               ' KAD: Deal with versions here based on VersionID
               'xPath = "//Version[@VersionID > " & verold.ToString & " and @VersionID <= " & verNew.ToString'//KnownChange"
               xPath = "//kc:KnownChange"
               nodeList = nodeMetadataChanges.SelectNodes(xPath, nsMgr)
               For Each node As Xml.XmlNode In nodeList
                  Me.List.Add(New KnownChange(node))
               Next
            End If
         End Sub

         Default Public Property Item(ByVal index As Int32) As KnownChange
            Get
               Return CType(Me.List(index), KnownChange)
            End Get
            Set(ByVal Value As KnownChange)
               Me.List(index) = Value
            End Set
         End Property
      End Class

      Private Class KnownChange
         Private mElementNames As ElementNames
         Private mChangeType As ChangeType
         Private mItemName As String
         Private mOutputXPathTest As String
         Private mNodeXPathTestLeft As String
         Private mNodeXPathTestRight As String

         Public Sub New(ByVal node As Xml.XmlNode)
            mElementNames = New ElementNames(Tools.GetAttributeOrEmpty(node, "ElementNames"))
            mChangeType = CType([Enum].Parse(GetType(ChangeType), Tools.GetAttributeOrEmpty(node, "ChangeType")), ChangeType)
            mItemName = Tools.GetAttributeOrEmpty(node, "ItemName")
            mOutputXPathTest = Tools.GetAttributeOrEmpty(node, "OutputXPathTest")
            mNodeXPathTestLeft = Tools.GetAttributeOrEmpty(node, "NodeXPathTestLeft")
            mNodeXPathTestRight = Tools.GetAttributeOrEmpty(node, "NodeXPathTestRight")
         End Sub

         Public ReadOnly Property ElementNames() As ElementNames
            Get
               Return mElementNames
            End Get
         End Property

         Public ReadOnly Property ChangeType() As ChangeType
            Get
               Return mChangeType
            End Get
         End Property

         Public ReadOnly Property ItemName() As String
            Get
               Return mItemName
            End Get
         End Property

         Public ReadOnly Property OutputXPathTest() As String
            Get
               Return mOutputXPathTest
            End Get
         End Property

         Public ReadOnly Property NodeXPathTestLeft() As String
            Get
               Return mNodeXPathTestLeft
            End Get
         End Property

         Public ReadOnly Property NodeXPathTestRight() As String
            Get
               Return mNodeXPathTestRight
            End Get
         End Property

      End Class

      Private Class ElementNames
         Inherits Collections.CollectionBase

         Public Sub New(ByVal elementNames As String)
            Dim names() As String = elementNames.Split(","c)
            For Each name As String In names
               name = name.Trim
               Me.List.Add(name)
            Next
         End Sub

         Public Function Contains(ByVal name As String) As Boolean
            Return Me.List.Contains(name)
         End Function

         Default Public Property Item(ByVal index As Int32) As String
            Get
               Return CType(Me.List(index), String)
            End Get
            Set(ByVal Value As String)
               Me.List(index) = Value
            End Set
         End Property

      End Class

      Private Class Record
         Private mChangeType As ChangeType
         Private mName As String
         Private mElementName As String
         Private mAttributes As New Collections.ArrayList
         Private mNode As Xml.XmlNode
         Private mXmlDoc As Xml.XmlDocument

         Public Sub New( _
                  ByVal xmlDoc As Xml.XmlDocument, _
                  ByVal changeType As ChangeType, _
                  ByVal elementName As String, _
                  ByVal name As String)
            mChangeType = changeType
            mName = name
            mElementName = elementName
            mXmlDoc = xmlDoc
         End Sub

         Public Sub AddAttribute(ByVal name As String, ByVal value As String)
            Dim attr As Xml.XmlAttribute = mXmlDoc.CreateAttribute(name)
            attr.Value = value
            mAttributes.Add(attr)
            If Not mNode Is Nothing Then
               mNode.Attributes.Append(attr)
            End If
         End Sub

         Public ReadOnly Property ChangeType() As ChangeType
            Get
               Return mChangeType
            End Get
         End Property

         Public ReadOnly Property Name() As String
            Get
               Return mName
            End Get
         End Property

         Public ReadOnly Property ElementName() As String
            Get
               Return mElementName
            End Get
         End Property

         Public ReadOnly Property Node() As Xml.XmlNode
            Get
               If mNode Is Nothing Then
                  mNode = xmlHelpers.NewElement(mXmlDoc, mChangeType.ToString, mName)
                  For Each attr As Xml.XmlAttribute In mAttributes
                     mNode.Attributes.Append(attr)
                  Next
               End If
               Return mNode
            End Get
         End Property
      End Class

#End Region

   End Class
End Namespace